﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Model.Interfaces;

namespace Model
{
    public static class SolutionMethod
    {
        #region Delegate

        public delegate void RefAction(IEquation equation, ref double arg0, ref double arg1, double arg2);

        #endregion

        #region Methods

        public static void RecalculateEuler(IEquation equation, ref double xk, ref double yk, double step)
        {
            yk += step * equation.Calculate(xk, yk);
            xk += step;
        }

        public static void RecalculateModifiedEuler(IEquation equation, ref double xk, ref double yk, double step)
        {
            yk += step * equation.Calculate(xk + step / 2, yk + step / 2 * equation.Calculate(xk, yk));
            xk += step;
        }

        public static void RecalculateRungeKutt(IEquation equation, ref double xk, ref double yk, double step)
        {
            var k1 = step * equation.Calculate(xk, yk);
            var k2 = step * equation.Calculate(xk + step / 2, yk + k1 / 2);
            var k3 = step * equation.Calculate(xk + step / 2, yk + k2 / 2);
            var k4 = step * equation.Calculate(xk + step, yk + k3);
            xk += step;
            yk += ((1.0 / 6.0) * (k1 + 2 * k2 + 2 * k3 + k4));
        }

        public static IEnumerable<Point> Calculate(ICauchySystem system, RefAction recalculateMethod, double step)
        {
            if (step < 0)
                throw new ArgumentException("step < 0");
            if (step == 0)
                throw new ArgumentException("step == 0");
            // get number of steps
            var count = (int)(Math.Abs(system.B - system.A) / step);
            // create resultant list
            var result = new List<Point>(count + 1)
                             {
                                 system.Initial
                             };

            var xk = system.Initial.X;
            var yk = system.Initial.Y;

            for (int k = 1; k < count + 1; ++k)
            {
                recalculateMethod(system.Equation, ref xk, ref yk, step);
                result.Add(new Point(xk, yk));
            }
            return result;
        }

        /// <summary>
        /// Aприорный способ нахождения шага
        /// </summary>
        /// <param name="system"></param>
        /// <param name="recalculateMethod"></param>
        /// <param name="step"></param>
        /// <returns></returns>
        private static IEnumerable<Point> CalculateWithAccuracy(ICauchySystem system, RefAction recalculateMethod, out double step)
        {
            // initial step
            step = Math.Abs(system.B - system.A)/2;
            // trying using list
            List<Point> currentValues = new List<Point>(), halfstepValues = new List<Point>();
            while(true)
            {
                // calculate amount of steps
                var count = (int)(Math.Abs(system.B - system.A) / step);
                // calculate current values
                currentValues.Add(system.Initial);

                // xk, yk
                double xk = system.Initial.X, yk = system.Initial.Y;
                for (int i = 1; i < count + 1; ++i)
                {
                    recalculateMethod(system.Equation, ref xk, ref yk, step);
                    currentValues.Add(new Point(xk, yk));
                }

                var halfStep = step/2;
                var halfstepCount = (int) (Math.Abs(system.B - system.A)/halfStep);
                halfstepValues.Add(system.Initial);
                xk = system.Initial.X;
                yk = system.Initial.Y;
                for (int i = 1; i < halfstepCount + 1; ++i)
                {
                    recalculateMethod(system.Equation, ref xk, ref yk, halfStep);
                    halfstepValues.Add(new Point(xk, yk));
                }

                // get difference
                double difference = 0;
                for (int i = 1; i < currentValues.Count; ++i)
                {
                    var diff = Math.Abs(currentValues[i].Y - halfstepValues[i*2].Y);
                    if (difference < diff)
                        difference = diff;
                }

                if (difference > system.Epsilon)
                {
                    step /= 2;
                    halfstepValues.Clear();
                    currentValues.Clear();
                }
                else
                {
                    return currentValues;
                }
            }

            
        }

        #region Euler

        public static IEnumerable<Point> EulerSolution(this ICauchySystem system, double step)
        {
            return Calculate(system, RecalculateEuler, step);
        }

        public static IEnumerable<Point> EulerSolutionWithAccuracy(this ICauchySystem system, out double step)
        {
            return CalculateWithAccuracy(system, RecalculateEuler, out step);
        }

        #endregion

        #region ModifiedEuler

        public static IEnumerable<Point> ModifiedEulerSolution(this ICauchySystem system, double step)
        {
            return Calculate(system, RecalculateModifiedEuler, step);
        }

        public static IEnumerable<Point> ModifiedEulerSolutionWithAccuracy(this ICauchySystem system, out double step)
        {
            return CalculateWithAccuracy(system, RecalculateModifiedEuler, out step);
        }

        #endregion

        #region Runge Kutt

        public static IEnumerable<Point> RungeKuttSolution(this ICauchySystem system, double step)
        {
            return Calculate(system, RecalculateRungeKutt, step);
        }

        public static IEnumerable<Point> RungeKuttSolutionWithAccuracy(this ICauchySystem system, out double step)
        {
            return CalculateWithAccuracy(system, RecalculateRungeKutt, out step);
        }

        #endregion

        #endregion
    }
}
